/*
 * This file is part of Cadmium.
 * Copyright (C) 2007-2010 Xavier Clerc.
 *
 * Cadmium is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * Cadmium is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package fr.x9c.cadmium.primitives.bigarray;

import java.awt.Toolkit;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.concurrent.atomic.AtomicBoolean;

import fr.x9c.cadmium.kernel.Block;
import fr.x9c.cadmium.kernel.Context;
import fr.x9c.cadmium.kernel.Custom;
import fr.x9c.cadmium.kernel.Fail;
import fr.x9c.cadmium.kernel.Fatal;
import fr.x9c.cadmium.kernel.Value;
import fr.x9c.cadmium.primitives.stdlib.Compare;
import fr.x9c.cadmium.util.IO;
import fr.x9c.cadmium.util.Misc;

/**
 * This class defines operations for custom type "_bigarray".
 *
 * @author <a href="mailto:cadmium@x9c.fr">Xavier Clerc</a>
 * @version 1.0
 * @since 1.0
 */
final class CustomBigArray extends Custom.AbstractOperations {

    /** Custom operations for "_bigarray". */
    static final Custom.Operations OPS = new CustomBigArray();

    /**
     * Only one instance of this class.
     */
    private CustomBigArray() {
        super("_bigarray", true, true, true);
    } // end empty constructor

    /**
     * {@inheritDoc}
     */
    @Override
    public void finalize(final Value v) {
        final MemArray b = (MemArray) v.asBlock().asCustom();
        switch (b.getFlags() & BigArray.CAML_BA_MANAGED_MASK) {
        case BigArray.CAML_BA_EXTERNAL:
            // nothing to do
            break;
        case BigArray.CAML_BA_MANAGED:
            assert false : "unsupported";
            break;
        case BigArray.CAML_BA_MAPPED_FILE:
            b.unMap();
            break;
        default:
            assert false : "invalid bigarray kind";
        } // end switch
    } // end method 'finalize(Value)'

    /**
     * {@inheritDoc}
     */
    @Override
    public int compare(final Value v1,
                       final Value v2,
                       final AtomicBoolean unordered) {
        final MemArray b1 = (MemArray) v1.asBlock().asCustom();
        final MemArray b2 = (MemArray) v2.asBlock().asCustom();
        final int[] dim1 = b1.getDims();
        final int[] dim2 = b2.getDims();

        if (dim1.length != dim2.length) {
            return dim2.length - dim1.length;
        } // end if
        for (int i = 0; i < dim1.length; i++) {
            final int d1 = dim1[i];
            final int d2 = dim2[i];
            if (d1 != d2) {
                return d1 < d2 ? Compare.LESS : Compare.GREATER;
            } // end if
        } // end for
        final long numElems = b1.getNumElems();
        final ByteBuffer d1 = b1.getData();
        final ByteBuffer d2 = b2.getData();
        switch (b1.getFlags() & BigArray.CAML_BA_KIND_MASK) {
        case BigArray.CAML_BA_COMPLEX32:
        case BigArray.CAML_BA_FLOAT32:
            final int mult32 = (b1.getFlags() & BigArray.CAML_BA_KIND_MASK) == BigArray.CAML_BA_COMPLEX32 ? 2 : 1;
            for (int i = 0; i < numElems * mult32; i++) {
                final float e1 = d1.getFloat(i * BigArray.SIZE_FLOAT32);
                final float e2 = d2.getFloat(i * BigArray.SIZE_FLOAT32);
                if (e1 < e2) {
                    return Compare.LESS;
                } else if (e1 > e2) {
                    return Compare.GREATER;
                } else if (e1 != e2) {
                    unordered.set(true);
                    if (e1 == e1) {
                        return Compare.GREATER;
                    } else if (e2 == e2) {
                        return Compare.LESS;
                    } // end if/elsif
                } // end if/elsif
            } // end for
            return Compare.EQUAL;
        case BigArray.CAML_BA_COMPLEX64:
        case BigArray.CAML_BA_FLOAT64:
            final int mult64 = (b1.getFlags() & BigArray.CAML_BA_KIND_MASK) == BigArray.CAML_BA_COMPLEX64 ? 2 : 1;
            for (int i = 0; i < numElems * mult64; i++) {
                final double e1 = d1.getDouble(i * BigArray.SIZE_FLOAT64);
                final double e2 = d2.getDouble(i * BigArray.SIZE_FLOAT64);
                if (e1 < e2) {
                    return Compare.LESS;
                } else if (e1 > e2) {
                    return Compare.GREATER;
                } else if (e1 != e2) {
                    unordered.set(true);
                    if (e1 == e1) {
                        return Compare.GREATER;
                    } else if (e2 == e2) {
                        return Compare.LESS;
                    } // end if/elsif
                } // end if/elsif
            } // end for
            return Compare.EQUAL;
        case BigArray.CAML_BA_SINT8:
            for (int i = 0; i < numElems; i++) {
                final byte e1 = d1.get(i * BigArray.SIZE_SINT8);
                final byte e2 = d2.get(i * BigArray.SIZE_SINT8);
                if (e1 < e2) {
                    return Compare.LESS;
                } else if (e1 > e2) {
                    return Compare.GREATER;
                } // end if/elsif
            } // end for
            return Compare.EQUAL;
        case BigArray.CAML_BA_UINT8:
            for (int i = 0; i < numElems; i++) {
                final int e1 =
                    Misc.signedToUnsignedByte(d1.get(i * BigArray.SIZE_UINT8));
                final int e2 =
                    Misc.signedToUnsignedByte(d2.get(i * BigArray.SIZE_UINT8));
                if (e1 < e2) {
                    return Compare.LESS;
                } else if (e1 > e2) {
                    return Compare.GREATER;
                } // end if/elsif
            } // end for
            return Compare.EQUAL;
        case BigArray.CAML_BA_SINT16:
            for (int i = 0; i < numElems; i++) {
                final short e1 = d1.getShort(i * BigArray.SIZE_SINT16);
                final short e2 = d2.getShort(i * BigArray.SIZE_SINT16);
                if (e1 < e2) {
                    return Compare.LESS;
                } else if (e1 > e2) {
                    return Compare.GREATER;
                } // end if/elsif
            } // end for
            return Compare.EQUAL;
        case BigArray.CAML_BA_UINT16:
            for (int i = 0; i < numElems; i++) {
                final int e1 =
                    Misc.signedToUnsignedShort(d1.getShort(i * BigArray.SIZE_UINT16));
                final int e2 =
                    Misc.signedToUnsignedShort(d2.getShort(i * BigArray.SIZE_UINT16));
                if (e1 < e2) {
                    return Compare.LESS;
                } else if (e1 > e2) {
                    return Compare.GREATER;
                } // end if/elsif
            } // end for
            return Compare.EQUAL;
        case BigArray.CAML_BA_INT64:
            for (int i = 0; i < numElems; i++) {
                final long e1 = d1.getLong(i * BigArray.SIZE_INT64);
                final long e2 = d2.getLong(i * BigArray.SIZE_INT64);
                if (e1 < e2) {
                    return Compare.LESS;
                } else if (e1 > e2) {
                    return Compare.GREATER;
                } // end if/elsif
            } // end for
            return Compare.EQUAL;
        case BigArray.CAML_BA_INT32:
        case BigArray.CAML_BA_CAML_INT:
        case BigArray.CAML_BA_NATIVE_INT:
            for (int i = 0; i < numElems; i++) {
                final int e1 = d1.getInt(i * BigArray.SIZE_INT32);
                final int e2 = d2.getInt(i * BigArray.SIZE_INT32);
                if (e1 < e2) {
                    return Compare.LESS;
                } else if (e1 > e2) {
                    return Compare.GREATER;
                } // end if/elsif
            } // end for
            return Compare.EQUAL;
        default:
            assert false : "invalid elements kind";
            return 0;
        } // end switch
    } // end method 'compare(Value, Value)'

    /**
     * {@inheritDoc}
     */
    @Override
    public int hash(final Value v) {
        final MemArray b = (MemArray) v.asBlock().asCustom();
        final ByteBuffer data = b.getData();
        final int numElems = Math.min(50, b.getNumElems());
        int h = 0;
        switch (b.getFlags() & BigArray.CAML_BA_KIND_MASK) {
        case BigArray.CAML_BA_SINT8:
        case BigArray.CAML_BA_UINT8:
            for (int i = 0; i < numElems; i++) {
                h = combine(h, Misc.signedToUnsignedByte(data.get(i * BigArray.SIZE_SINT8)));
            } // end for
            break;
        case BigArray.CAML_BA_SINT16:
        case BigArray.CAML_BA_UINT16:
            for (int i = 0; i < numElems; i++) {
                h = combine(h, Misc.signedToUnsignedShort(data.getShort(i * BigArray.SIZE_SINT16)));
            } // end for
            break;
        case BigArray.CAML_BA_FLOAT32:
        case BigArray.CAML_BA_COMPLEX32:
        case BigArray.CAML_BA_INT32:
        case BigArray.CAML_BA_CAML_INT:
        case BigArray.CAML_BA_NATIVE_INT:
            for (int i = 0; i < numElems; i++) {
                h = combine(h, data.getInt(i * BigArray.SIZE_INT32));
            } // end for
            break;
        case BigArray.CAML_BA_FLOAT64:
        case BigArray.CAML_BA_COMPLEX64:
        case BigArray.CAML_BA_INT64:
            for (int i = 0; i < numElems; i++) {
                h = combine(h, data.getInt(i * BigArray.SIZE_FLOAT64 + (BigArray.SIZE_FLOAT64 / 2)));
                h = combine(h, data.getInt(i * BigArray.SIZE_FLOAT64));
            } // end for
            break;
        default:
            assert false : "invalid bigarray kind";
        } // end switch
        return h;
    } // end method 'hash(Value)'

    /**
     * {@inheritDoc}
     */
    @Override
    public Custom.SerializationSizes serialize(final DataOutput out,
                                               final Value v)
        throws IOException {
        final MemArray b = (MemArray) v.asBlock().asCustom();
        final ByteBuffer data = b.getData();
        final int[] dims = b.getDims();
        IO.write32s(out, dims.length);
        IO.write32s(out, b.getFlags()
                    & (BigArray.CAML_BA_KIND_MASK | BigArray.CAML_BA_LAYOUT_MASK));
        int numElems = 1;
        for (int i = 0; i < dims.length; i++) {
            final int d = dims[i];
            IO.write32s(out, d);
            numElems *= d;
        } // end for
        switch (b.getFlags() & BigArray.CAML_BA_KIND_MASK) {
        case BigArray.CAML_BA_SINT8:
        case BigArray.CAML_BA_UINT8:
            for (int i = 0; i < numElems; i++) {
                IO.write8s(out, data.get(i * BigArray.SIZE_SINT8));
            } // end for
            break;
        case BigArray.CAML_BA_SINT16:
        case BigArray.CAML_BA_UINT16:
            for (int i = 0; i < numElems; i++) {
                IO.write16s(out, data.getShort(i * BigArray.SIZE_SINT16));
            } // end for
            break;
        case BigArray.CAML_BA_FLOAT32:
        case BigArray.CAML_BA_INT32:
            for (int i = 0; i < numElems; i++) {
                IO.write32s(out, data.getInt(i * BigArray.SIZE_INT32));
            } // end for
            break;
        case BigArray.CAML_BA_COMPLEX32:
            for (int i = 0; i < numElems * 2; i++) {
                IO.write32s(out, data.getInt(i * BigArray.SIZE_FLOAT32));
            } // end for
            break;
        case BigArray.CAML_BA_FLOAT64:
        case BigArray.CAML_BA_INT64:
            for (int i = 0; i < numElems; i++) {
                IO.write64s(out, data.getLong(i * BigArray.SIZE_INT64));
            } // end for
            break;
        case BigArray.CAML_BA_COMPLEX64:
            for (int i = 0; i < numElems * 2; i++) {
                IO.write64s(out, data.getLong(i * BigArray.SIZE_FLOAT64));
            } // end for
            break;
        case BigArray.CAML_BA_CAML_INT:
        case BigArray.CAML_BA_NATIVE_INT:
            IO.write8s(out, 0);
            for (int i = 0; i < numElems; i++) {
                IO.write32s(out, data.getInt(i * BigArray.SIZE_CAML_INT));
            } // end for
            break;
        default:
            assert false : "invalid bigarray kind";
        } // end switch
        return new Custom.SerializationSizes(4 + dims.length * 4,
                                             4 + dims.length * 8);
    } // end method 'serialize(DataOutput, Value)'

    /**
     * {@inheritDoc}
     */
    @Override
    public long deserialize(final DataInput in, final Block dst)
        throws Fail.Exception, Fatal.Exception, IOException {
        final int numDims = Misc.ensure32s(IO.read32u(in));
        final int flags = IO.read32s(in);
        final int[] dim = new int[numDims];
        long numElems = 1;
        for (int i = 0; i < numDims; i++) {
            final int d = Misc.ensure32s(IO.read32u(in));
            dim[i] = d;
            numElems *= d;
        } // end for
        if ((flags & BigArray.CAML_BA_KIND_MASK) > BigArray.CAML_BA_COMPLEX64) {
            Fail.failWith("input_value: bad bigarray kind");
        } // end if
        final MemArray b = new MemArray(flags, dim, null);
        final ByteBuffer data = b.getData();
        dst.setCustom(b);
        switch (b.getFlags() & BigArray.CAML_BA_KIND_MASK) {
        case BigArray.CAML_BA_SINT8:
        case BigArray.CAML_BA_UINT8:
            for (int i = 0; i < numElems; i++) {
                data.put(i * BigArray.SIZE_SINT8, (byte) IO.read8s(in));
            } // end for
            break;
        case BigArray.CAML_BA_SINT16:
        case BigArray.CAML_BA_UINT16:
            for (int i = 0; i < numElems; i++) {
                data.putShort(i * BigArray.SIZE_SINT16, (short) IO.read16s(in));
            } // end for
            break;
        case BigArray.CAML_BA_FLOAT32:
        case BigArray.CAML_BA_INT32:
            for (int i = 0; i < numElems; i++) {
                data.putInt(i * BigArray.SIZE_INT32, IO.read32s(in));
            } // end for
            break;
        case BigArray.CAML_BA_COMPLEX32:
            for (int i = 0; i < numElems * 2; i++) {
                data.putInt(i * BigArray.SIZE_FLOAT32, IO.read32s(in));
            } // end for
            break;
        case BigArray.CAML_BA_FLOAT64:
        case BigArray.CAML_BA_INT64:
            for (int i = 0; i < numElems; i++) {
                data.putLong(i * BigArray.SIZE_FLOAT64, IO.read64s(in));
            } // end for
            break;
        case BigArray.CAML_BA_COMPLEX64:
            for (int i = 0; i < numElems * 2; i++) {
                data.putLong(i * BigArray.SIZE_FLOAT64, IO.read64s(in));
            } // end for
            break;
        case BigArray.CAML_BA_CAML_INT:
        case BigArray.CAML_BA_NATIVE_INT:
            if (IO.read8u(in) != 0) {
                Fail.failWith("input_value: cannot read bigarray with 64-bit Caml ints");
            } // end if
            for (int i = 0; i < numElems; i++) {
                data.putInt(i * BigArray.SIZE_CAML_INT, IO.read32s(in));
            } // end for
            break;
        } // end switch
        return BigArray.STRUCT_SIZE + (b.getNumDims() - 1) * 4; // "funny" size
    } // end method 'deserialize(DataInput, Block)'

    /**
     * Combines integer values; used in hashing.
     * @param h current hash value
     * @param v value to combine with
     * @return new hash value
     */
    private static int combine(final int h, final int v) {
        return  (h << 4) + h + v;
    } // end method 'combine(int, int)'

} // end class 'CustomBigArray'
